/*
 * $Id: JsonRpcServiceExporter.java 63219 2007-07-24 13:22:20Z jdamick $
 *
 * Copyright 2006- Revolution Health Group.  All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Revolution Health Group.  (Confidential Information).
 * You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license 
 * agreement you entered into with Revolution Health Group.
 *
 */
package org.javason.jsonrpc;

import static org.javason.jsonrpc.JsonRpcConstants.ERROR;
import static org.javason.jsonrpc.JsonRpcConstants.ID;
import static org.javason.jsonrpc.JsonRpcConstants.MODEL_KEY;
import static org.javason.jsonrpc.JsonRpcConstants.SYSTEM_INSPECTOR;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

import org.javason.jsonrpc.JsonRpcError.JsonRpcErrorCode;
import org.javason.jsonrpc.impl.JsonRpcInvocationRequest;
import org.javason.jsonrpc.impl.JsonRpcInvocationRequestFactory;
import org.javason.jsonrpc.impl.JsonRpcOperationInvoker;
import org.javason.jsonrpc.impl.JsonRpcService;
import org.javason.jsonrpc.impl.JsonRpcSystemOperationInvoker;
import org.javason.util.Debug;
import org.javason.util.Metrics;

/**
 * @author Jeffrey Damick
 *
 * 
 * <b>Add the following to your application context for spring:</b>
 * <code>
 * <bean id="beanNameViewResolver" 
 *		class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
 *	
 *  
 *	<bean id="jsonRpcServiceView" class="org.javason.jsonrpc.JsonRpcServiceView"/>	
 *  <bean id="jsonRpcServiceInterfaceView" class="org.javason.jsonrpc.JsonRpcServiceInterfaceView"/>
 * </code>
 * 
 * Mapping:
 * HTTP METHOD -> prefix (for implemented methods)
 * POST -> save
 * GET -> find
 * PUT -> save
 * DELETE -> delete
 * HEAD -> find
 */
public class JsonRpcServiceExporter extends AbstractController {
	private static final String HTTP_POST = "POST"; // create - save
	private static final String HTTP_GET = "GET"; // read - find
	private static final String HTTP_PUT = "PUT"; // update - save
	private static final String HTTP_DELETE = "DELETE"; // delete - destroy
	private static final String HTTP_HEAD = "HEAD"; // read - find
	private static final String HTTP_OPTIONS = "OPTIONS"; // list of methods
	private static final String BEAN_VIEW = "jsonRpcServiceView"; // default
    private static final String DESCRIPTION_VIEW = "jsonRpcServiceDescriptionView";
	private static final Logger _log = Logger.getLogger(JsonRpcServiceExporter.class);
	public static final String VERSION = "$Rev: 63219 $";
    
	private JsonRpcService _service = null;
	private Object _serviceBean = null;
	private Class _serviceClass = null;
	private JsonRpcOperationInvoker _invoker = null;
	private JsonRpcHandler	_handler = null;
	
	
	public JsonRpcServiceExporter() {
		super();
		setSupportedMethods(new String[] {HTTP_POST, HTTP_GET, HTTP_PUT, HTTP_DELETE, HTTP_HEAD, HTTP_OPTIONS});
	}

	public void setJsonRpcHandler(JsonRpcHandler JsonRpcHandler) {
		_handler = JsonRpcHandler;
	}
	
	public void setServiceBean(Object serviceBean) {
		_serviceBean = serviceBean;
	}
	
	public void setServiceClass(Class serviceClass) {
		_serviceClass = serviceClass;
	}
	
	private void initializeService() {
		if (_service == null) {
			_service = new JsonRpcService(_serviceBean, _serviceClass);
			_invoker = new JsonRpcOperationInvoker(_service);
		}
	}
	
	private String getRequestContent(HttpServletRequest request) throws UnsupportedEncodingException, IOException {
		BufferedReader input = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
		
		StringBuilder inputString = new StringBuilder();
		String line = input.readLine();
		while (line != null) {
			inputString.append(line);
			line = input.readLine();
		}
		return inputString.toString();
	}
	
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request, 
			HttpServletResponse response) throws Exception {
		
		Metrics timer = Metrics.beginCollection();
		
		Map<String, Object> model = new HashMap<String, Object>();
		ModelAndView result = new ModelAndView(BEAN_VIEW, model);
		try {
			// lazy initialize the operations (if no JsonRpcriction class is set)
			initializeService();		
	
			int length = request.getContentLength();
			if (request.getMethod().equalsIgnoreCase(HTTP_POST) && length > 0) {
				try {
					JsonRpcInvocationRequest serviceReq = JsonRpcInvocationRequestFactory.createPostRequest(_service, request);
					if (invokeMethod(result, serviceReq, request)) {
						response.setStatus(HttpServletResponse.SC_OK);
						Debug.log(_log, "Success executing %s", serviceReq.getMethodName());
					}
					
				} catch (NoSuchMethodException noMethodException) {
					Debug.log(_log, noMethodException, "Error finding method");
	 				setResultError(result, JsonRpcErrorCode.PROC_NOT_FOUND, noMethodException);
	 			} catch (Exception exception) {
	 				Debug.log(_log, exception, "Error Invoking method");
	 				setResultError(result, JsonRpcErrorCode.SERVICE_ERROR, exception);
				}
			} else if (request.getMethod().equalsIgnoreCase(HTTP_GET)) {
				try {
					invokeMethod(result, JsonRpcInvocationRequestFactory.createGetRequest(_service, request), request);
				} catch (NoSuchMethodException noMethodException) {
					Debug.log(_log, noMethodException, "Error finding method");
	 				setResultError(result, JsonRpcErrorCode.PROC_NOT_FOUND, noMethodException);
	 				sendError(response, noMethodException);
	 			} catch (Exception exception) {
	 				Debug.log(_log, exception, "Error Invoking method");
	 				setResultError(result, JsonRpcErrorCode.SERVICE_ERROR, exception);
	 				sendError(response, exception);
				}
			} else if (length == 0) {
				IllegalArgumentException exception = new IllegalArgumentException("0 length request content");
				Debug.log(_log, exception, "Error Invoking method content: %s", getRequestContent(request));
				setResultError(result, JsonRpcErrorCode.PARSE_ERROR, exception);
			} else {
				IllegalArgumentException exception = new IllegalArgumentException("Unsupported Option");
				Debug.log(_log, exception, "Error Invoking method");
				setResultError(result, JsonRpcErrorCode.PARSE_ERROR, exception);
			}
		} finally {
			timer.endAndLogCollection();
		}
		return result;
	}
	
	
	private boolean invokeMethod(ModelAndView result, 
			JsonRpcInvocationRequest serviceReq, HttpServletRequest request) throws Exception {
		
		boolean invoked = false;
		
		if (serviceReq.getMethodName().equals(SYSTEM_INSPECTOR)) {
            result.getModel().put("model", _service);
            // render inspector.jsp
            result.setViewName("jsonrpc-inspector");
            Debug.log(_log, "sending back system inspector");
        } else if (serviceReq.isSystemMethod()) {
        	JsonRpcSystemOperationInvoker invoker = new JsonRpcSystemOperationInvoker(_service);
			String jsonOutput = (String) invoker.invoke(serviceReq);
        	result.getModel().put("model", jsonOutput);
            result.setViewName(DESCRIPTION_VIEW);
            Debug.log(_log, "sending back system description");
		} else if (serviceReq.getMethodName().length() > 0) {
			if (_handler != null) {
				_handler.invoke(serviceReq.getMethodName(), null, serviceReq.getMessageContext());
			}
			
			Object model = _invoker.invoke(serviceReq);
			
			// opaque id
			Object requestId = serviceReq.getId();
			result.getModel().put(ID, requestId);
			
			result.getModel().put(MODEL_KEY, model);
			result.getModel().put(ERROR, new JsonRpcError()); // for 1.1 should be omitted
			invoked = true;
		} else {
			throw new NoSuchMethodException("Empty Method Name");
		}
		return invoked;
	}
	
	private void sendError(HttpServletResponse response, Exception exception) {
		Debug.log(_log, "caught error, sending error back");
		response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		try {
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 
					exception.getMessage());
		} catch (IOException e) {
			e.printStackTrace();//ok, we're still sending back server error..
		}
	}
	
	private void setResultError(ModelAndView result, JsonRpcErrorCode errorCode, 
			Throwable exception) {
		
		JsonRpcError error = errorCode.createJsonRpcError();
		error.setError(exception);
		result.getModel().put(ERROR, error);
	}
}
